# HG changeset patch # User MrJuneJune # Date 1786180088 25200 # Node ID e02e2036ef8465c0b60aa7c4aab8f3b6707e0628 # Parent 41a49c29a28f5ddf9e38e72a8ed67aff98237cb0 add Layer 2 JRPG component system Add reusable content and window modals, an isolated component sandbox, shared cyberpunk scroll areas, production-safe cache freshness, and server-rendered JRPG panel state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> diff -r 41a49c29a28f -r e02e2036ef84 mrjunejune/PWA_SETUP.md --- a/mrjunejune/PWA_SETUP.md Fri Aug 07 16:05:29 2026 -0700 +++ b/mrjunejune/PWA_SETUP.md Sat Aug 08 02:08:08 2026 -0700 @@ -58,7 +58,7 @@ ✅ **Offline Support** - Caches pages, CSS, JS, fonts, images ✅ **App Shortcuts** - Quick access to Blog and Notes ✅ **Install Prompt** - Automatic install button -✅ **Auto-updates** - Service worker updates on reload +✅ **Auto-updates** - New service workers activate and reload clients automatically ✅ **Fast Loading** - Cached resources load instantly ## Customization @@ -72,7 +72,13 @@ Edit `sw.js` to change: - `CACHE_VERSION` - Increment to force cache refresh - `STATIC_CACHE` - Files to cache immediately -- Caching strategy (currently: cache-first with network fallback) +- Caching strategy: + - HTML, CSS, JS, JSON, and WASM are network-first with offline cache fallback. + - Fonts and images are stale-while-revalidate. + - Component sandbox assets always bypass service-worker caching. + +Unversioned application code must not use cache-first. That can preserve stale +production CSS or JavaScript indefinitely when a deployment reuses the same URL. ## Testing on Mobile diff -r 41a49c29a28f -r e02e2036ef84 mrjunejune/main.c --- a/mrjunejune/main.c Fri Aug 07 16:05:29 2026 -0700 +++ b/mrjunejune/main.c Sat Aug 08 02:08:08 2026 -0700 @@ -1214,12 +1214,145 @@ return resp; } +typedef struct { + const char *name; + const char *title; + const char *copy; + const char *works; +} Jrpg_Initial_Panel; + +static boolean Mjj_Replace_All( + char *buffer, + size_t capacity, + const char *needle, + const char *replacement) +{ + if (!buffer || !needle || !replacement || needle[0] == '\0') + return FALSE; + + size_t needle_length = strlen(needle); + size_t replacement_length = strlen(replacement); + char *match = strstr(buffer, needle); + while (match) + { + size_t current_length = strlen(buffer); + size_t offset = (size_t)(match - buffer); + size_t new_length = + current_length - needle_length + replacement_length; + if (new_length >= capacity) + return FALSE; + + memmove( + match + replacement_length, + match + needle_length, + current_length - offset - needle_length + 1); + memcpy(match, replacement, replacement_length); + match = strstr(match + replacement_length, needle); + } + return TRUE; +} + +static const Jrpg_Initial_Panel *Jrpg_Resolve_Initial_Panel( + Seobeo_Request_Entry *req) +{ + static const Jrpg_Initial_Panel panels[] = { + { + "resume", + "Resume", + "Member of Technical Staff and engineering leader with 10+ years " + "building AI agent platforms and production systems across Microsoft, " + "Meta, Google, and growth-stage companies.", + "
  • " + "Full resumeCareer dossier
  • " + "
  • " + "Copilot TasksAgentic execution
  • " + }, + { + "tools", + "Tools", + "Useful browser tools backed by first-party C, WASM, media, and " + "document-processing systems.", + "
  • " + "MarkdownWriting
  • " + "
  • " + "ConverterMedia
  • " + "
  • " + "HLS PlayerStreaming
  • " + }, + { + "blog", + "Blogs", + "Technical writing about networking, rendering, performance, developer " + "tooling, and experiments.", + "
  • " + "All postsArchive
  • " + }, + { + "conversations", + "Resume", + "", + "" + }, + }; + + const char *requested = NULL; + void *panel_kv = Dowa_HashMap_Get_Ptr(req, "query_panel"); + if (panel_kv) + requested = ((Seobeo_Request_Entry *)panel_kv)->value; + + for (size_t i = 0; i < sizeof(panels) / sizeof(panels[0]); ++i) + { + if (requested && strcmp(requested, panels[i].name) == 0) + return &panels[i]; + } + return &panels[0]; +} + Seobeo_Request_Entry *GetJrpg(Seobeo_Request_Entry *req, Dowa_Arena *arena) { Seobeo_Request_Entry *resp = NULL; char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP); if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/jrpg/index.html", arena)) return html_render_error(arena, "Internal Server Error"); + + const Jrpg_Initial_Panel *panel = Jrpg_Resolve_Initial_Panel(req); + boolean conversations = + strcmp(panel->name, "conversations") == 0 ? TRUE : FALSE; + const char *preview_selection = conversations ? "resume" : panel->name; + struct { + const char *token; + const char *value; + } replacements[] = { + {"__MJJ_PANEL__", panel->name}, + {"__MJJ_PREVIEW_SELECTION__", preview_selection}, + {"__MJJ_PREVIEW_TITLE__", panel->title}, + {"__MJJ_PREVIEW_COPY__", panel->copy}, + {"__MJJ_PREVIEW_WORKS__", panel->works}, + {"__MJJ_PREVIEW_HIDDEN__", conversations ? "hidden" : ""}, + {"__MJJ_ARCHIVE_HIDDEN__", conversations ? "" : "hidden"}, + {"__MJJ_RESUME_PRESSED__", + strcmp(panel->name, "resume") == 0 ? "true" : "false"}, + {"__MJJ_TOOLS_PRESSED__", + strcmp(panel->name, "tools") == 0 ? "true" : "false"}, + {"__MJJ_BLOG_PRESSED__", + strcmp(panel->name, "blog") == 0 ? "true" : "false"}, + {"__MJJ_CONVERSATIONS_PRESSED__", conversations ? "true" : "false"}, + }; + for (size_t i = 0; + i < sizeof(replacements) / sizeof(replacements[0]); + ++i) + { + if (!Mjj_Replace_All( + final_body, + HTML_PAGE_CAP, + replacements[i].token, + replacements[i].value)) + return html_render_error(arena, "Internal Server Error"); + } Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); Dowa_HashMap_Push_Arena(resp, "referrer-policy", "no-referrer", arena); return resp; diff -r 41a49c29a28f -r e02e2036ef84 mrjunejune/src/jrpg/index.html --- a/mrjunejune/src/jrpg/index.html Fri Aug 07 16:05:29 2026 -0700 +++ b/mrjunejune/src/jrpg/index.html Sat Aug 08 02:08:08 2026 -0700 @@ -7,13 +7,18 @@ + +
    - +
    @@ -49,61 +54,67 @@ - - - - - -
    - +

    Menu

    + + + +
    + +
    +
    +
    + +
    +

    Experience, projects, and the systems I have helped build.

    - - - +
    + + + +
    -
    diff -r 41a49c29a28f -r e02e2036ef84 mrjunejune/src/jrpg/jrpg.css --- a/mrjunejune/src/jrpg/jrpg.css Fri Aug 07 16:05:29 2026 -0700 +++ b/mrjunejune/src/jrpg/jrpg.css Sat Aug 08 02:08:08 2026 -0700 @@ -41,7 +41,15 @@ --zenbu-sys-font-family-ui: "Pixel Mplus"; --zenbu-sys-font-family-content: "Pixel Mplus"; --zenbu-sys-font-family-code: "Pixel Mplus"; + --zenbu-sys-type-caption-family: "Pixel Mplus"; --zenbu-sys-type-label-family: "Pixel Mplus"; + --zenbu-sys-type-body-family: "Pixel Mplus"; + --zenbu-sys-type-body-large-family: "Pixel Mplus"; + --zenbu-sys-type-subtitle-family: "Pixel Mplus"; + --zenbu-sys-type-title-family: "Pixel Mplus"; + --zenbu-sys-type-page-title-family: "Pixel Mplus"; + --zenbu-sys-type-reading-family: "Pixel Mplus"; + --zenbu-sys-type-code-family: "Pixel Mplus"; min-width: 20rem; min-height: 100dvh; overflow: hidden; @@ -741,38 +749,6 @@ box-sizing: border-box; width: min(82vw, 46rem); max-width: calc(100% - var(--zenbu-sys-space-content)); - padding: var(--zenbu-sys-padding-xl); - border: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-frame); - border-radius: 0; - background: var(--mjj-jrpg-surface-raised); - color: var(--mjj-jrpg-text); - box-shadow: - inset 0 0 0 var(--zenbu-sys-stroke-width) - var(--mjj-jrpg-accent), - var(--zenbu-sys-space-control) - var(--zenbu-sys-space-control) - 0 - var(--mjj-jrpg-accent); -} - -.jrpg-preview-dialog dialog::backdrop { - background: color-mix(in srgb, var(--mjj-jrpg-canvas) 84%, transparent); -} - -.jrpg-preview-dialog dialog[open] { - animation: mjj-jrpg-dialog-open var(--zenbu-sys-motion-duration-layout) - var(--zenbu-sys-motion-ease-enter); -} - -@keyframes mjj-jrpg-dialog-open { - from { - clip-path: inset(48% 25%); - transform: scaleX(0.35); - } - to { - clip-path: inset(0); - transform: scaleX(1); - } } .jrpg-dialog-heading { @@ -780,8 +756,6 @@ align-items: center; justify-content: space-between; gap: var(--zenbu-sys-space-group); - padding-bottom: var(--zenbu-sys-padding-md); - border-bottom: var(--zenbu-sys-stroke-width) solid var(--mjj-jrpg-frame-soft); } .jrpg-dialog-heading p, @@ -800,8 +774,13 @@ } .jrpg-dialog-heading zen-button > :where(button, a) { + box-sizing: border-box; width: auto; min-width: var(--zenbu-control-height); + height: var(--zenbu-control-height); + padding: + var(--zenbu-control-padding-block) + var(--zenbu-control-padding-inline); border-color: var(--mjj-jrpg-accent); background: var(--mjj-jrpg-surface); color: var(--mjj-jrpg-text); @@ -812,40 +791,32 @@ padding: 0; } -.jrpg-preview-dialog dialog h2 { - margin-top: var(--zenbu-sys-space-content); -} - -.jrpg-preview-dialog dialog > p { - margin-bottom: var(--zenbu-sys-space-content); -} - .jrpg-preview-dialog dialog.jrpg-detail-dialog { - width: min(94vw, 72rem); + width: min(92vw, 60rem); max-height: calc(100dvh - var(--zenbu-sys-space-container)); overflow: hidden; } .jrpg-preview-dialog dialog.jrpg-detail-dialog[open] { display: grid; - grid-template-rows: auto auto minmax(0, 1fr) auto; + grid-template-rows: auto minmax(0, 1fr) auto; gap: var(--zenbu-sys-space-group); } +mjj-window-modal .jrpg-detail-content { + box-sizing: border-box; + height: 100%; + min-height: 0; + padding: var(--zenbu-sys-padding-lg); +} + +mjj-content-modal .jrpg-detail-content { + padding: var(--zenbu-sys-padding-lg); +} + .jrpg-preview-dialog dialog.jrpg-detail-dialog[data-detail-mode="tools"] { - width: calc(100vw - var(--zenbu-sys-space-content)); - max-width: none; - height: calc(100dvh - var(--zenbu-sys-space-content)); - max-height: none; - padding: 0; - border: 0; - background: transparent; - box-shadow: none; -} - -.jrpg-preview-dialog dialog.jrpg-detail-dialog[data-detail-mode="tools"][open] { - grid-template-rows: auto minmax(0, 1fr); - gap: var(--zenbu-sys-space-control); + width: min(90vw, 56rem); + height: min(78dvh, 44rem); } .jrpg-detail-dialog[data-detail-mode="tools"] > h2 { @@ -854,19 +825,14 @@ .jrpg-detail-dialog[data-detail-mode="tools"] .jrpg-dialog-heading { padding: var(--zenbu-sys-padding-sm); - border: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-frame); - background: var(--mjj-jrpg-surface-raised); - box-shadow: - inset 0 0 0 var(--zenbu-sys-stroke-width) - var(--mjj-jrpg-accent); } .jrpg-resume-dossier { min-height: 0; - padding: var(--zenbu-sys-padding-lg); + padding: 0; overflow: auto; - border: var(--zenbu-sys-stroke-width) solid var(--mjj-jrpg-frame-soft); - background: var(--mjj-jrpg-surface-sunken); + border: 0; + background: transparent; scrollbar-color: var(--mjj-jrpg-frame) transparent; } @@ -901,9 +867,10 @@ .jrpg-resume-dossier .info { display: grid; gap: var(--zenbu-sys-space-control); - padding: var(--zenbu-sys-padding-lg); - border: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-primary); - background: var(--mjj-jrpg-panel); + padding: 0 0 var(--zenbu-sys-padding-lg); + border: 0; + border-bottom: var(--zenbu-sys-stroke-width) solid var(--mjj-jrpg-frame-soft); + background: transparent; } .jrpg-resume-dossier .info > p { @@ -972,6 +939,10 @@ text-underline-offset: var(--zenbu-sys-space-icon-label); } +.jrpg-resume-dossier a[href="/public/resume.pdf"] { + display: none; +} + .jrpg-dialog-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -1127,56 +1098,18 @@ } .jrpg-tool-panes { - position: relative; box-sizing: border-box; display: grid; - grid-template-columns: - minmax(0, 1fr) - calc(var(--zenbu-sys-space-container) * 2) - minmax(0, 1fr); + grid-template-columns: minmax(0, 1fr); + grid-template-rows: repeat(2, minmax(18rem, auto)); gap: var(--zenbu-sys-space-control); - height: 100%; + height: auto; min-height: 0; padding: var(--zenbu-sys-padding-sm); } .jrpg-tool-panes::before { - grid-column: 2; - grid-row: 1; - border: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-frame); - background-image: - radial-gradient( - circle at 50% var(--zenbu-sys-space-content), - var(--mjj-jrpg-highlight) 0 var(--zenbu-sys-stroke-width-emphasis), - var(--mjj-jrpg-frame) var(--zenbu-sys-stroke-width-emphasis) - calc(var(--zenbu-sys-stroke-width-emphasis) * 2), - transparent calc(var(--zenbu-sys-stroke-width-emphasis) * 2) - ), - radial-gradient( - circle at 50% calc(100% - var(--zenbu-sys-space-content)), - var(--mjj-jrpg-highlight) 0 var(--zenbu-sys-stroke-width-emphasis), - var(--mjj-jrpg-frame) var(--zenbu-sys-stroke-width-emphasis) - calc(var(--zenbu-sys-stroke-width-emphasis) * 2), - transparent calc(var(--zenbu-sys-stroke-width-emphasis) * 2) - ), - linear-gradient( - 90deg, - var(--mjj-jrpg-surface-sunken), - var(--mjj-jrpg-surface-raised) 45%, - var(--mjj-jrpg-frame) 50%, - var(--mjj-jrpg-surface-raised) 55%, - var(--mjj-jrpg-surface-sunken) - ), - var(--mjj-jrpg-art); - background-position: center; - background-size: auto, auto, auto, cover; - box-shadow: - inset 0 0 0 var(--zenbu-sys-stroke-width) - var(--mjj-jrpg-info), - 0 0 var(--zenbu-sys-space-related) - color-mix(in srgb, var(--mjj-jrpg-primary) 65%, transparent); - content: ""; - image-rendering: pixelated; + display: none; } .jrpg-tool-window:first-of-type { @@ -1185,8 +1118,8 @@ } .jrpg-tool-window:last-of-type { - grid-column: 3; - grid-row: 1; + grid-column: 1; + grid-row: 2; } .jrpg-tool-pane { @@ -1981,26 +1914,10 @@ box-sizing: border-box; min-width: min(22rem, calc(100vw - var(--zenbu-sys-space-content))); max-width: min(26rem, calc(100vw - var(--zenbu-sys-space-content))); - padding: var(--zenbu-sys-padding-lg); - border: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-frame); - border-radius: 0; - background: var(--mjj-jrpg-surface-raised); - color: var(--mjj-jrpg-text); - box-shadow: - inset 0 0 0 var(--zenbu-sys-stroke-width) var(--mjj-jrpg-accent), - var(--zenbu-sys-space-control) var(--zenbu-sys-space-control) 0 var(--mjj-jrpg-accent); } -.jrpg-archive-dialog::backdrop { - background: color-mix(in srgb, var(--mjj-jrpg-canvas) 84%, transparent); -} - -.jrpg-archive-dialog[open] { - display: grid; - grid-template-rows: auto auto auto; - gap: var(--zenbu-sys-space-group); - animation: mjj-jrpg-dialog-open var(--zenbu-sys-motion-duration-layout) - var(--zenbu-sys-motion-ease-enter); +mjj-window-modal [data-archive-rename-dialog] { + height: auto; } .jrpg-archive-dialog-title { @@ -2276,7 +2193,7 @@ } /* Hamburger button: transparent overlay over baked art */ - .jrpg-mobile-destination-owner > .jrpg-mobile-menu-btn { + .jrpg-mobile-destination-owner > zen-dialog > .jrpg-mobile-menu-btn { display: grid; place-items: center; position: absolute; @@ -2381,10 +2298,13 @@ cursor: pointer; } - .jrpg-mobile-destination-content { + mjj-window-modal .jrpg-mobile-destination-content { min-width: 0; min-height: 0; - overflow: hidden; + padding: 0; + overflow-x: auto; + overflow-y: scroll; + scrollbar-gutter: stable; } .jrpg-mobile-destination-content > :is(mjj-jrpg-menu, .jrpg-utility) { @@ -2538,27 +2458,7 @@ .jrpg-login-dialog { box-sizing: border-box; width: min(24rem, calc(100vw - var(--zenbu-sys-space-content))); - padding: var(--zenbu-sys-padding-xl); - border: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-frame); - border-radius: 0; - background: var(--mjj-jrpg-surface-raised); - color: var(--mjj-jrpg-text); - box-shadow: - inset 0 0 0 var(--zenbu-sys-stroke-width) var(--mjj-jrpg-accent), - var(--zenbu-sys-space-control) var(--zenbu-sys-space-control) 0 - var(--mjj-jrpg-accent); -} - -.jrpg-login-dialog::backdrop { - background: color-mix(in srgb, var(--mjj-jrpg-canvas) 84%, transparent); -} - -.jrpg-login-dialog[open] { - display: grid; - grid-template-rows: auto auto minmax(0, 1fr); - gap: var(--zenbu-sys-space-group); - animation: mjj-jrpg-dialog-open var(--zenbu-sys-motion-duration-layout) - var(--zenbu-sys-motion-ease-enter); + height: auto; } .jrpg-login-dialog-header { @@ -2566,8 +2466,6 @@ align-items: center; justify-content: space-between; gap: var(--zenbu-sys-space-group); - padding-bottom: var(--zenbu-sys-padding-md); - border-bottom: var(--zenbu-sys-stroke-width) solid var(--mjj-jrpg-frame-soft); } .jrpg-login-dialog-header h2 { diff -r 41a49c29a28f -r e02e2036ef84 mrjunejune/src/jrpg/jrpg.js --- a/mrjunejune/src/jrpg/jrpg.js Fri Aug 07 16:05:29 2026 -0700 +++ b/mrjunejune/src/jrpg/jrpg.js Sat Aug 08 02:08:08 2026 -0700 @@ -543,6 +543,7 @@ class MjjJrpgPreview extends HTMLElement { connectedCallback() { + this._modalOwner = this.querySelector("[data-detail-modal-owner]"); this._dialog = this.querySelector("dialog"); this._onDialogClose = () => { this._toolRequest = (this._toolRequest || 0) + 1; @@ -570,30 +571,32 @@ const latestBlog = event.target.closest("[data-latest-blog]"); if (latestBlog && !event.metaKey && !event.ctrlKey && !event.shiftKey) { event.preventDefault(); - this.querySelector("zen-dialog").open(); + this.show("blog"); + this._modalOwner?.open(); void this.loadBlogDetail(latestBlog.dataset.blogUrl); return; } const blogArchive = event.target.closest("[data-blog-archive]"); if (blogArchive) { event.preventDefault(); - this.querySelector("zen-dialog").open(); this.show("blog"); + this._modalOwner?.open(); void this.loadBlogs(); return; } const resumeModal = event.target.closest("[data-resume-modal]"); if (resumeModal) { event.preventDefault(); - this.querySelector("zen-dialog").open(); this.show("resume"); + this._modalOwner?.open(); void this.loadResume(); return; } const latestTool = event.target.closest("[data-latest-tool]"); if (latestTool && !event.metaKey && !event.ctrlKey && !event.shiftKey) { event.preventDefault(); - this.querySelector("zen-dialog").open(); + this.show("tools"); + this._modalOwner?.open(); void this.loadToolDetail(latestTool.dataset.toolUrl); } }; @@ -608,6 +611,18 @@ this.cleanupTool(); } + _setDetailModalType(useWindow) { + const targetName = useWindow ? "mjj-window-modal" : "mjj-content-modal"; + if (!this._modalOwner || this._modalOwner.localName === targetName) return; + const replacement = document.createElement(targetName); + for (const attribute of [...this._modalOwner.attributes]) { + replacement.setAttribute(attribute.name, attribute.value); + } + replacement.append(...this._modalOwner.childNodes); + this._modalOwner.replaceWith(replacement); + this._modalOwner = replacement; + } + show(selection) { this.cleanupTool(); const preview = PREVIEWS[selection] || PREVIEWS.resume; @@ -617,9 +632,6 @@ this.querySelector("[data-dialog-title]").textContent = preview.title; this.querySelector("[data-dialog-copy]").textContent = preview.copy; this.renderShowcase(preview.works); - const link = this.querySelector("[data-preview-link]"); - link.href = preview.url; - link.setAttribute("aria-label", `Open ${preview.title}`); const resumeDossier = this.querySelector("[data-resume-dossier]"); const resumeDownload = this.querySelector("[data-resume-download]"); const blogBrowser = this.querySelector("[data-blog-browser]"); @@ -627,10 +639,10 @@ const isResume = selection === "resume"; const isBlog = selection === "blog"; const isTools = selection === "tools"; + this._setDetailModalType(isTools); this._dialog.dataset.detailMode = isTools ? "tools" : "content"; resumeDossier.hidden = !isResume; resumeDownload.hidden = !isResume; - this.querySelector("[data-dialog-actions]").hidden = !isResume; blogBrowser.hidden = !isBlog; toolBrowser.hidden = !isTools; this.querySelector("[data-dialog-copy]").hidden = @@ -711,6 +723,12 @@ const resume = documentCopy.querySelector("main"); if (!resume) throw new Error("Resume content is unavailable"); sanitizeFetchedMain(resume); + for (const duplicateDownload of resume.querySelectorAll( + 'a[href="/public/resume.pdf"]', + )) { + duplicateDownload.closest("zen-button")?.remove(); + duplicateDownload.remove(); + } content.replaceChildren(...resume.childNodes); status.hidden = true; this._resumeLoaded = true; @@ -823,9 +841,6 @@ status.textContent = `Loading ${blog.label}...`; content.replaceChildren(); this.querySelector("[data-dialog-title]").textContent = blog.label; - const fullPage = this.querySelector("[data-preview-link]"); - fullPage.href = blog.url; - fullPage.setAttribute("aria-label", `Open ${blog.label}`); for (const button of this.querySelectorAll("[data-blog-entry]")) { button.setAttribute( "aria-pressed", @@ -928,9 +943,6 @@ this.cleanupTool(); content.replaceChildren(); this.querySelector("[data-dialog-title]").textContent = tool.label; - const fullPage = this.querySelector("[data-preview-link]"); - fullPage.href = tool.url; - fullPage.setAttribute("aria-label", `Open ${tool.label}`); if (tool.url === "/tools/markdown_to_html") { await this.renderMarkdownTool(content, status, request); return; @@ -1426,10 +1438,12 @@ this._loadMoreButton = this.querySelector("[data-archive-load-more]"); this._newButton = this.querySelector("[data-archive-new]"); this._closeButton = this.querySelector("[data-archive-close]"); + this._deleteModal = this.querySelector("[data-archive-delete-owner]"); this._deleteDialog = this.querySelector("[data-archive-delete-dialog]"); this._deleteNameEl = this.querySelector("[data-archive-delete-name]"); this._deleteCancelButton = this.querySelector("[data-archive-delete-cancel]"); this._deleteConfirmButton = this.querySelector("[data-archive-delete-confirm]"); + this._renameModal = this.querySelector("[data-archive-rename-owner]"); this._renameDialog = this.querySelector("[data-archive-rename-dialog]"); this._renameInput = this.querySelector("[data-archive-rename-input]"); this._renameForm = this.querySelector("[data-archive-rename-form]"); @@ -1452,7 +1466,7 @@ }); this._deleteCancelButton?.addEventListener("click", () => { - this._deleteDialog?.close(); + this._deleteModal?.close(); const t = this._pendingDeleteTrigger; this._pendingDeleteId = null; this._pendingDeleteTrigger = null; @@ -1462,7 +1476,7 @@ this._deleteConfirmButton?.addEventListener("click", () => { const id = this._pendingDeleteId; const trigger = this._pendingDeleteTrigger; - this._deleteDialog?.close(); + this._deleteModal?.close(); this._pendingDeleteId = null; this._pendingDeleteTrigger = null; if (id) { @@ -1481,7 +1495,7 @@ }); this._renameCancelButton?.addEventListener("click", () => { - this._renameDialog?.close(); + this._renameModal?.close(); const t = this._pendingRenameTrigger; this._pendingRenameId = null; this._pendingRenameTrigger = null; @@ -1494,7 +1508,7 @@ const trigger = this._pendingRenameTrigger; const newTitle = this._renameInput?.value.trim(); if (!id || !newTitle) return; - this._renameDialog?.close(); + this._renameModal?.close(); this._pendingRenameId = null; this._pendingRenameTrigger = null; this.dispatchEvent(new CustomEvent("mjj-archive-rename", { @@ -1529,7 +1543,7 @@ this._pendingRenameId = item._convId; this._pendingRenameTrigger = renameButton; if (this._renameInput) this._renameInput.value = item._convTitle || ""; - this._renameDialog?.showModal(); + this._renameModal?.open(); requestAnimationFrame(() => { this._renameInput?.select(); }); } return; @@ -1541,7 +1555,7 @@ this._pendingDeleteId = item._convId; this._pendingDeleteTrigger = deleteButton; if (this._deleteNameEl) this._deleteNameEl.textContent = item._convTitle || ""; - this._deleteDialog?.showModal(); + this._deleteModal?.open(); requestAnimationFrame(() => { this._deleteConfirmButton?.focus(); }); } return; @@ -2186,7 +2200,10 @@ let characterTimer = 0; let currentConversationId = null; - let currentPanel = "resume"; + const serverInitialPanel = VALID_PANELS.includes(shell?.dataset.initialPanel) + ? shell.dataset.initialPanel + : "resume"; + let currentPanel = serverInitialPanel; let archiveCursor = null; let activeController = null; let archiveLoadInFlight = false; /* serialize pagination requests */ @@ -2799,7 +2816,7 @@ normalUrl, ); } - currentPanel = validPanel || "resume"; + currentPanel = validPanel || serverInitialPanel; } /* ---- Load archive; only an explicit URL restores a conversation ---- */ diff -r 41a49c29a28f -r e02e2036ef84 mrjunejune/src/public/component-sandbox.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/public/component-sandbox.css Sat Aug 08 02:08:08 2026 -0700 @@ -0,0 +1,203 @@ +@font-face { + font-family: "Pixel Mplus"; + src: url("/public/fonts/pixel-mplus-12-regular.ttf") format("truetype"); + font-display: block; +} + +:root { + --zenbu-sys-font-family-ui: "Pixel Mplus"; + --zenbu-sys-font-family-content: "Pixel Mplus"; + --zenbu-sys-font-family-code: "Pixel Mplus"; + --zenbu-sys-type-caption-family: "Pixel Mplus"; + --zenbu-sys-type-label-family: "Pixel Mplus"; + --zenbu-sys-type-body-family: "Pixel Mplus"; + --zenbu-sys-type-body-large-family: "Pixel Mplus"; + --zenbu-sys-type-subtitle-family: "Pixel Mplus"; + --zenbu-sys-type-title-family: "Pixel Mplus"; + --zenbu-sys-type-page-title-family: "Pixel Mplus"; + --zenbu-sys-type-reading-family: "Pixel Mplus"; + --zenbu-sys-type-code-family: "Pixel Mplus"; +} + +body { + box-sizing: border-box; + min-height: 100dvh; + margin: 0; + padding: var(--zenbu-sys-padding-xl); + background: var(--zenbu-sys-color-surface-page); + color: var(--zenbu-sys-color-text-primary); + font-family: var(--zenbu-sys-font-family-ui); +} + +body * { + font-family: inherit; +} + +zen-icon svg { + shape-rendering: crispEdges; + stroke-linecap: square; + stroke-linejoin: miter; +} + +main { + display: grid; + gap: var(--zenbu-sys-space-container); + width: min(100%, 64rem); + margin-inline: auto; +} + +.sandbox-heading { + display: grid; + gap: var(--zenbu-sys-space-control); +} + +.sandbox-heading h1 { + color: var(--zenbu-sys-color-action-primary-background); + text-shadow: + var(--zenbu-sys-space-icon-label) + var(--zenbu-sys-space-icon-label) + 0 + var(--zenbu-sys-color-danger-foreground); +} + +.sandbox-heading :where(h1, p), +.sandbox-card :where(h2, p), +.sandbox-pane :where(h3, p) { + margin: 0; +} + +.sandbox-heading p, +.sandbox-card > p, +.sandbox-muted { + color: var(--zenbu-sys-color-text-secondary); +} + +.sandbox-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--zenbu-sys-space-group); +} + +.sandbox-layers { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--zenbu-sys-space-control); + margin: 0; +} + +.sandbox-layers > div { + display: grid; + gap: var(--zenbu-sys-space-icon-label); + padding: var(--zenbu-sys-padding-md); + border: var(--zenbu-sys-stroke-width) solid + var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-control); + background: var(--zenbu-sys-color-surface-sunken); +} + +.sandbox-layers > div:nth-child(1) dt { + color: var(--zenbu-sys-color-warning-foreground); +} + +.sandbox-layers > div:nth-child(2) dt { + color: var(--zenbu-sys-color-info-foreground); +} + +.sandbox-layers > div:nth-child(3) dt { + color: var(--zenbu-sys-color-action-primary-background); +} + +.sandbox-layers dt { + color: var(--zenbu-sys-color-text-primary); + font-weight: var(--zenbu-sys-font-weight-strong); +} + +.sandbox-layers dd { + margin: 0; + color: var(--zenbu-sys-color-text-secondary); + font-size: var(--zenbu-sys-font-size-sm); +} + +.sandbox-rule { + padding: var(--zenbu-sys-padding-md); + border-inline-start: var(--zenbu-sys-stroke-width-emphasis) solid + var(--zenbu-sys-color-action-primary-border); + background: var(--zenbu-sys-color-surface-sunken); + color: var(--zenbu-sys-color-text-secondary); +} + +.sandbox-card { + display: grid; + align-content: start; + gap: var(--zenbu-sys-space-group); + padding: var(--zenbu-sys-padding-xl); + border: var(--zenbu-sys-stroke-width) solid + var(--zenbu-sys-color-border-default); + border-radius: var(--zenbu-sys-radius-container); + background: var(--zenbu-sys-color-surface-raised); + box-shadow: + var(--zenbu-sys-space-control) + var(--zenbu-sys-space-control) + 0 + var(--zenbu-sys-color-danger-foreground); +} + +.sandbox-card > h2 { + color: var(--zenbu-sys-color-action-primary-background); +} + +.sandbox-list { + display: grid; + gap: var(--zenbu-sys-space-control); + margin: 0; + padding: 0; + list-style: none; +} + +.sandbox-list li { + padding-bottom: var(--zenbu-sys-padding-sm); + border-bottom: var(--zenbu-sys-stroke-width) solid + var(--zenbu-sys-color-border-subtle); +} + +.sandbox-audit { + display: grid; + gap: var(--zenbu-sys-space-control); + margin: var(--zenbu-sys-space-group) 0 0; + padding: 0; + list-style: none; +} + +.sandbox-audit li { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: var(--zenbu-sys-space-control); + padding-block: var(--zenbu-sys-padding-sm); + border-top: var(--zenbu-sys-stroke-width) solid + var(--zenbu-sys-color-border-subtle); + color: var(--zenbu-sys-color-text-secondary); +} + +.sandbox-audit strong { + color: var(--zenbu-sys-color-warning-foreground); +} + +.sandbox-pane { + display: grid; + align-content: start; + gap: var(--zenbu-sys-space-control); +} + +@media (max-width: 42rem) { + body { + padding: var(--zenbu-sys-padding-lg); + } + + .sandbox-grid { + grid-template-columns: minmax(0, 1fr); + } + + .sandbox-layers { + grid-template-columns: minmax(0, 1fr); + } +} diff -r 41a49c29a28f -r e02e2036ef84 mrjunejune/src/public/component-sandbox.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/public/component-sandbox.html Sat Aug 08 02:08:08 2026 -0700 @@ -0,0 +1,263 @@ + + + + + + AI Component Sandbox | MrJuneJune + + + + + + + +
    +
    +

    Isolated browser fixture

    +

    AI Component Sandbox

    +

    + Edit one component family, refresh this page, and inspect desktop or + mobile behavior without application chrome. +

    +
    +
    +
    Layer 0 · Foundation
    +
    API mapping, i18n, and semantic themes.
    +
    +
    +
    Layer 1 · Primitives
    +
    Zenbu icons, fields, buttons, and dialog behavior.
    +
    +
    +
    Layer 2 · Composites
    +
    MrJuneJune composers and modal families.
    +
    +
    +

    + Dependency rule: Layer 2 may consume Layers 1 and 0. Application + routes, persistence, and business state stay above this sandbox. +

    +
    + +
    +
    +

    Content modal

    +

    + Editorial reading surface for blogs, notes, release details, and + long-form content. +

    + + + + + + +
    +
    +

    Engineering note · 8 min read

    +

    Designing durable agent interfaces

    +
    +
    + + + +
    +
    + +
    +

    + A content modal should feel like a focused reading room: + enough structure to orient the reader, but no window chrome + competing with the article. +

    +

    Start with a strict shell

    +

    + Header, body, and footer are stable regions. The article + owns its semantic headings, paragraphs, lists, and code. + The modal only supplies rhythm, containment, scrolling, + and depth. +

    +
    + Content should remain ordinary HTML even when the + presentation becomes cinematic. +
    +

    Keep the body readable

    +

    + The reading column stays bounded while the dialog itself + can grow. This avoids very long lines on desktop and keeps + padding deliberate on small screens. +

    +

    Use a visible scroll channel

    +

    + Cyan marks the active rail, violet separates the track, + and magenta appears on hover. Keyboard users can focus the + region and scroll without moving the modal chrome. +

    +

    + The header and footer stay fixed while this article moves. + That stable frame keeps context visible during longer blog + posts, documentation, and generated explanations. +

    +

    + Layer 2 only chooses layout and presentation. The Layer 1 + scroll primitive still owns the native overflow behavior. +

    +
    <mjj-content-modal>
    +  <header data-modal-header>...</header>
    +  <zen-scroll-area data-modal-body data-cyber-scroll>
    +    ...
    +  </zen-scroll-area>
    +  <footer data-modal-footer>...</footer>
    +</mjj-content-modal>
    +
    +
    +
    +

    Updated August 7, 2026

    +
    + + + +
    +
    +
    +
    +
    +
    + +
    +

    Window modal

    +

    + Structured workspace for tools and multi-region interfaces with + fixed chrome and a bounded scrolling body. +

    + + + + + + +
    +
    +

    Workspace · Read only

    +

    Component inspector

    +
    +
    + + + + + + +
    +
    + + + + + +
    +

    Body content

    +

    + The window body can host split panes, editors, previews, + or tools without changing the surrounding contract. +

    + + + + The active light-DOM fixture. + +
      +
    1. 01 Header region validated
    2. +
    3. 02 Title association generated
    4. +
    5. 03 Body region bounded
    6. +
    7. 04 Scroll primitive connected
    8. +
    9. 05 Footer actions discovered
    10. +
    11. 06 Pixel typography inherited
    12. +
    13. 07 Cyberpunk theme resolved
    14. +
    15. 08 Keyboard scrolling enabled
    16. +
    17. 09 Responsive split verified
    18. +
    19. 10 Layer contract ready
    20. +
    +
    +
    +
    +
    +

    3 required regions · contract valid

    +
    + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + + diff -r 41a49c29a28f -r e02e2036ef84 mrjunejune/src/public/composer-lab.html --- a/mrjunejune/src/public/composer-lab.html Fri Aug 07 16:05:29 2026 -0700 +++ b/mrjunejune/src/public/composer-lab.html Sat Aug 08 02:08:08 2026 -0700 @@ -7,9 +7,21 @@ - - +